import XCTest
import GRDB

class FoundationDateComponentsTests : GRDBTestCase {
    
    override func setup(_ dbWriter: some DatabaseWriter) throws {
        var migrator = DatabaseMigrator()
        migrator.registerMigration("createDates") { db in
            try db.execute(sql: """
                CREATE TABLE dates (
                    ID INTEGER PRIMARY KEY,
                    creationDate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)
                """)
        }
        try migrator.migrate(dbWriter)
    }
    
    func testDatabaseDateComponentsFormatHM() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .HM)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "10:11")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.HM)
            XCTAssertTrue(databaseDateComponents.dateComponents.year == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.month == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.day == nil)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertTrue(databaseDateComponents.dateComponents.second == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatHMS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .HMS)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "10:11:12")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.HMS)
            XCTAssertTrue(databaseDateComponents.dateComponents.year == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.month == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.day == nil)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, dateComponents.second)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatHMSS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .HMSS)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "10:11:12.123")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.HMSS)
            XCTAssertTrue(databaseDateComponents.dateComponents.year == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.month == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.day == nil)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, dateComponents.second)
            XCTAssertEqual(round(Double(databaseDateComponents.dateComponents.nanosecond!) / 1.0e6), round(Double(dateComponents.nanosecond!) / 1.0e6))
        }
    }

    func testDatabaseDateComponentsFormatYMD() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .YMD)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "1973-09-18")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertTrue(databaseDateComponents.dateComponents.hour == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.minute == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.second == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatYMD_HM() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .YMD_HM)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "1973-09-18 10:11")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HM)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertTrue(databaseDateComponents.dateComponents.second == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatYMD_HMS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .YMD_HMS)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "1973-09-18 10:11:12")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HMS)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, dateComponents.second)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatYMD_HMSS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .YMD_HMSS)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "1973-09-18 10:11:12.123")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HMSS)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, dateComponents.second)
            XCTAssertEqual(round(Double(databaseDateComponents.dateComponents.nanosecond!) / 1.0e6), round(Double(dateComponents.nanosecond!) / 1.0e6))
        }
    }

    func testUndefinedDatabaseDateComponentsFormatYMD_HMSS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            let dateComponents = DateComponents()
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: [DatabaseDateComponents(dateComponents, format: .YMD_HMSS)])
            
            let string = try String.fetchOne(db, sql: "SELECT creationDate from dates")!
            XCTAssertEqual(string, "0000-01-01 00:00:00.000")
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HMSS)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, 0)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, 1)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, 1)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, 0)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, 0)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, 0)
            XCTAssertEqual(databaseDateComponents.dateComponents.nanosecond, 0)
        }
    }

    func testDatabaseDateComponentsFormatIso8601YMD_HM() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: ["1973-09-18T10:11"])
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HM)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertTrue(databaseDateComponents.dateComponents.second == nil)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatIso8601YMD_HMS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: ["1973-09-18T10:11:12"])
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HMS)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, dateComponents.second)
            XCTAssertTrue(databaseDateComponents.dateComponents.nanosecond == nil)
        }
    }

    func testDatabaseDateComponentsFormatIso8601YMD_HMSS() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            
            var dateComponents = DateComponents()
            dateComponents.year = 1973
            dateComponents.month = 9
            dateComponents.day = 18
            dateComponents.hour = 10
            dateComponents.minute = 11
            dateComponents.second = 12
            dateComponents.nanosecond = 123_456_789
            try db.execute(sql: "INSERT INTO dates (creationDate) VALUES (?)", arguments: ["1973-09-18T10:11:12.123"])
            
            let databaseDateComponents = try DatabaseDateComponents.fetchOne(db, sql: "SELECT creationDate FROM dates")!
            XCTAssertEqual(databaseDateComponents.format, DatabaseDateComponents.Format.YMD_HMSS)
            XCTAssertEqual(databaseDateComponents.dateComponents.year, dateComponents.year)
            XCTAssertEqual(databaseDateComponents.dateComponents.month, dateComponents.month)
            XCTAssertEqual(databaseDateComponents.dateComponents.day, dateComponents.day)
            XCTAssertEqual(databaseDateComponents.dateComponents.hour, dateComponents.hour)
            XCTAssertEqual(databaseDateComponents.dateComponents.minute, dateComponents.minute)
            XCTAssertEqual(databaseDateComponents.dateComponents.second, dateComponents.second)
            XCTAssertEqual(round(Double(databaseDateComponents.dateComponents.nanosecond!) / 1.0e6), round(Double(dateComponents.nanosecond!) / 1.0e6))
        }
    }

    func testFormatYMD_HMSIsLexicallyComparableToCURRENT_TIMESTAMP() throws {
        let dbQueue = try makeDatabaseQueue()
        try dbQueue.inDatabase { db in
            var calendar = Calendar(identifier: .gregorian)
            calendar.timeZone = TimeZone(secondsFromGMT: 0)!
            do {
                let date = Date().addingTimeInterval(-1)
                let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
                try db.execute(
                    sql: "INSERT INTO dates (id, creationDate) VALUES (?,?)",
                    arguments: [1, DatabaseDateComponents(dateComponents, format: .YMD_HMS)])
            }
            do {
                try db.execute(
                    sql: "INSERT INTO dates (id) VALUES (?)",
                    arguments: [2])
            }
            do {
                let date = Date().addingTimeInterval(1)
                let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
                try db.execute(
                    sql: "INSERT INTO dates (id, creationDate) VALUES (?,?)",
                    arguments: [3, DatabaseDateComponents(dateComponents, format: .YMD_HMS)])
            }
            
            let ids = try Int.fetchAll(db, sql: "SELECT id FROM dates ORDER BY creationDate")
            XCTAssertEqual(ids, [1,2,3])
        }
    }

    func testDatabaseDateComponentsParsing() {
        func assertParse(_ string: String, _ dateComponent: DatabaseDateComponents, file: StaticString = #file, line: UInt = #line) {
            do {
                // Test DatabaseValueConvertible adoption
                guard let parsed = DatabaseDateComponents.fromDatabaseValue(string.databaseValue) else {
                    XCTFail("Could not parse \(String(reflecting: string))", file: file, line: line)
                    return
                }
                XCTAssertEqual(parsed.format, dateComponent.format, file: file, line: line)
                XCTAssertEqual(parsed.dateComponents, dateComponent.dateComponents, file: file, line: line)
            }
            do {
                // Test StatementColumnConvertible adoption
                guard let parsed = try? DatabaseQueue().inDatabase({
                    try DatabaseDateComponents.fetchOne($0, sql: "SELECT ?", arguments: [string])
                }) else {
                    XCTFail("Could not parse \(String(reflecting: string))", file: file, line: line)
                    return
                }
                XCTAssertEqual(parsed.format, dateComponent.format, file: file, line: line)
                XCTAssertEqual(parsed.dateComponents, dateComponent.dateComponents, file: file, line: line)
            }
        }
        
        assertParse(
            "0000-01-01",
            DatabaseDateComponents(
                DateComponents(year: 0, month: 1, day: 1, hour: nil, minute: nil, second: nil, nanosecond: nil),
                format: .YMD))
        assertParse(
            "2018-12-31",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 12, day: 31, hour: nil, minute: nil, second: nil, nanosecond: nil),
                format: .YMD))
        assertParse(
            "2018-04-21 00:00",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21 00:00Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21 00:00+00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21 00:00-00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21 00:00+01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 4500),
                    year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21 00:00-01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: -4500),
                    year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21T23:59",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .YMD_HM))
        assertParse(
            "2018-04-21 00:00:00",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: nil),
                format: .YMD_HMS))
        assertParse(
            "2018-04-21T23:59:59",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: nil),
                format: .YMD_HMS))
        assertParse(
            "2018-04-21 00:00:00.0",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 0),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.9",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.9Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21 00:00:00.00",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 0),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21 00:00:00.01",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 10_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.99",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.99Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21 00:00:00.000",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 0),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21 00:00:00.001",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 0, minute: 0, second: 0, nanosecond: 1_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999+00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999-00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999+01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 4500),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999-01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: -4500),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999123",
            DatabaseDateComponents(
                DateComponents(year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "2018-04-21T23:59:59.999123Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: 2018, month: 04, day: 21, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .YMD_HMSS))
        assertParse(
            "00:00",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "23:59",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "23:59Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "23:59+00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "23:59-00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "23:59+01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 4500),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "23:59-01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: -4500),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: nil, nanosecond: nil),
                format: .HM))
        assertParse(
            "00:00:00",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: nil),
                format: .HMS))
        assertParse(
            "23:59:59",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: nil),
                format: .HMS))
        assertParse(
            "00:00:00.0",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 0),
                format: .HMSS))
        assertParse(
            "23:59:59.9",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.9Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 900_000_000),
                format: .HMSS))
        assertParse(
            "00:00:00.00",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 0),
                format: .HMSS))
        assertParse(
            "00:00:00.01",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 10_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.99",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.99Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 990_000_000),
                format: .HMSS))
        assertParse(
            "00:00:00.000",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 0),
                format: .HMSS))
        assertParse(
            "00:00:00.001",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 0, minute: 0, second: 0, nanosecond: 1_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999+00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999-00:00",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999+01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 4500),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999-01:15",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: -4500),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999123",
            DatabaseDateComponents(
                DateComponents(year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
        assertParse(
            "23:59:59.999123Z",
            DatabaseDateComponents(
                DateComponents(
                    timeZone: TimeZone(secondsFromGMT: 0),
                    year: nil, month: nil, day: nil, hour: 23, minute: 59, second: 59, nanosecond: 999_000_000),
                format: .HMSS))
    }
    
    func testDatabaseDateComponentsFromUnparsableString() {
        let databaseDateComponents = DatabaseDateComponents.fromDatabaseValue("foo".databaseValue)
        XCTAssertTrue(databaseDateComponents == nil)
    }

    func testJSONEncodingOfDatabaseDateComponents() throws {
        // Encoding root string is not supported by all system version: use an object
        struct Record: Encodable {
            var date: DatabaseDateComponents
        }
        let record = Record(date: DatabaseDateComponents(DateComponents(year: 2018, month: 12, day: 31), format: .YMD))
        let jsonData = try JSONEncoder().encode(record)
        let json = String(data: jsonData, encoding: .utf8)!
        XCTAssertEqual(json, """
            {"date":"2018-12-31"}
            """)
    }

    func testJSONDecodingOfDatabaseDateComponents() throws {
        // Decoding root string is not supported by all system version: use an object
        struct Record: Decodable {
            var date: DatabaseDateComponents
        }
        let json = """
            {"date":"2018-12-31"}
            """
        let record = try JSONDecoder().decode(Record.self, from: json.data(using: .utf8)!)
        XCTAssertEqual(record.date.format, .YMD)
        XCTAssertEqual(record.date.dateComponents, DateComponents(year: 2018, month: 12, day: 31))
    }
}
